#include "unpack.hpp"
#include <cstring>
#include <boost/scoped_array.hpp>
#include "debug.hpp"

namespace fur
{
   namespace
   {
      const uint8_t ldecode[] =
      { 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56,
	64, 80, 96, 112, 128, 160, 192, 224
      };
      const uint8_t lbits[] =
      { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4,
	5, 5, 5, 5
      };
      const int ddecode[] =
      { 0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384,
	512, 768, 1024, 1536, 2048, 3072, 4096, 6144, 8192, 12288, 16384, 24576,
	32768, 49152, 65536,98304, 131072, 196608, 262144, 327680, 393216,
	458752, 524288, 589824, 655360, 720896, 786432, 851968, 917504, 983040,
	1048576, 1310720, 1572864, 1835008, 2097152, 2359296, 2621440, 2883584,
	3145728, 3407872, 3670016, 3932160
      };
      const uint8_t dbits[] =
      { 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10,
	10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 16, 16, 16, 16, 16,
	16, 16, 16, 16, 16, 16, 16, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18,
	18, 18
      };

      const int dbitlengthcounts[] =
      { 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 14, 0, 12 };
      const uint8_t sddecode[] = { 0, 4, 8, 16, 32, 64, 128, 192 };
      const uint8_t sdbits[] = { 2, 2, 3, 4, 5, 6, 6, 6 };
   }
   Unpack::Unpack(std::ifstream &infile, std::ofstream &outfile, const FileHead &filehead) throw(Exception)
      :bitinput(infile, 0x1000),
       window(),
       fw(outfile)
   {
      filecrc = filehead.get_filecrc();
      version = filehead.get_version();
      writtenposi = 0;
   }

   Unpack::~Unpack() throw()
   {
   }

   void Unpack::do_unpack()
   {
      switch (version)
      {
	 case 29:unpack_29();break;
	 default:break;
      }
   }

   void Unpack::insert_old_dist(size_t distance) throw()
   {
      olddist[3] = olddist[2];
      olddist[2] = olddist[1];
      olddist[1] = olddist[0];
      olddist[0] = distance;
   }

   void Unpack::insert_last_match(size_t distance, size_t size) throw()
   {
      lastdist = distance;
      lastsize = size;
   }

   bool Unpack::read_table()
   {
      uint8_t bitlength[BC];
      uint8_t table[HTS];
      uint16_t bitfield;
      int i;
      int length;
      int zerocount;
      int number;
      int n;
      if (bitinput.avail_size() < 25 )
      {
	 if (!bitinput.read())
	 {
	    return false;
	 }
      }
      bitinput.align();
      bitfield = bitinput.getbits_8();
      if (bitfield & 0x80)
      {
	 throw EUnsupport(__func__);
      }
      if (!(bitfield & 0x40))
      {
	 memset(oldtable, 0, sizeof(oldtable));
      }
      bitinput.addbits(2);
      for (i = 0; i < BC; ++i)
      {
	 length = bitinput.getbits_8() >> 4;
	 bitinput.addbits(4);
	 if (length == 15)
	 {
	    zerocount = bitinput.getbits_8() >> 4;
	    bitinput.addbits(4);
	    if (zerocount == 0)
	    {
	       bitlength[i] = 15;
	    }
	    else
	    {
	       zerocount += 1;
	       bitlength[i] = 0;
	       while (zerocount > 0 && i < BC)
	       {
		  ++i;
		  --zerocount;
		  bitlength[i] = 0;
	       }
	    }
	 }
	 else
	 {
	    bitlength[i] = length;  
	 }
      }
      bitdecode.make_table(bitlength, BC);
      for (i = 0; i < HTS;)
      {
	 if (bitinput.avail_size() < 5)
	 {
	    if (!bitinput.read())
	    {
	       return false;
	    }
	 }
	 number = bitdecode.get_number(bitinput);
	 if (number < 16)
	 {
	    table[i] = (number + oldtable[i]) & 0xf;
	    ++i;
	 }
	 else
	 {
	    if (number < 18)
	    {
	       if (number == 16)
	       {
		  n = (bitinput.getbits_8() >> 5) + 3;
		  bitinput.addbits(3);
	       }
	       else
	       {
		  n = (bitinput.getbits_8() >> 1) + 11;
		  bitinput.addbits(7);
	       }
	       while (n > 0 && i < HTS)
	       {
		  --n;
		  table[i] = table[i - 1];
		  ++i;
	       }
	    }
	    else
	    {
	       if (number == 18)
	       {
		  n = (bitinput.getbits_8() >> 5) + 3;
		  bitinput.addbits(3);
	       }
	       else
	       {
		  n = (bitinput.getbits_8() >> 1) + 11;
		  bitinput.addbits(7);
	       }
	       while (n > 0 && i < HTS)
	       {
		  --n;
		  table[i] = 0;
		  ++i;
	       }
	    }
	 } // if (number < 16) ... else {
      } // for (ki = 0; i < HTS; ) {
      litdecode.make_table(table, NC);
      distdecode.make_table(table + NC, DC);
      lowdistdecode.make_table(table + NC + DC, LDC);
      repdecode.make_table(table + NC + DC + LDC, RC);
      memcpy(oldtable, table, sizeof(oldtable));
      return true;
   }

   bool Unpack::read_end_of_block()
   {
      uint16_t bitfield = bitinput.getbits_8();
      if (bitfield & 0x80)
      {
	 bitinput.addbits(1);
	 return read_table();
      }
      else
      {
	 ;
	 return false;
      }
   }

   bool Unpack::read_vm()
   {
      uint16_t first_byte = bitinput.getbits_8();
      uint16_t length = (first_byte & 7) + 1;
      int i;
      bool status;
      bitinput.addbits(8);
      if (length == 7)
      {
	 length = bitinput.getbits_8() + 7;
	 bitinput.addbits(8);
      }
      else
      {
	 if (length == 8)
	 {
	    length = bitinput.getbits_16();
	    bitinput.addbits(16);
	 }
      }
      boost::scoped_array<uint8_t> vmcode(new uint8_t[length]);
      for (i = 0; i < length; ++i)
      {
	 if (bitinput.avail_size() <= 1 && i < length - 1)
	 {
	    return false;
	 }
	 vmcode[i] = bitinput.getbits_8();
	 bitinput.addbits(8);
      }
      status = add_vm(first_byte, vmcode.get(), length);
      return status;
   }

   bool Unpack::add_vm(uint16_t firstbyte, uint8_t *vmcode, uint16_t size)
   {
      BitInputBase input(vmcode, size);
      uint filtpos;
      if (firstbyte & 0x80)
      {
	 filtpos = vm_read_data(input);
	 if (filtpos == 0)
	    initfilters();
	 else
	    --filtpos;
      }
      else
      {
	 filtpos = lastfilter;
      }
      if ( filtpos > filters.size() || filtpos > oldfilterlengths.size() )
      {
	 return false;
      }
      lastfilter = filtpos;
      bool newfilter = (filtpos == filters.size());
      Filter *stackfilter = new Filter;
      Filter *filter;
      if (newfilter)
      {
	 if (filtpos > 1024)
	 {
	    return false;
	 }
	 filter = new Filter;
	 filters.push_back(filter);
	 stackfilter->parent = filters.size() - 1;
	 oldfilterlengths.push_back(1);
	 filter->count = 0;
      }
      else
      {
	 filter = filters[filtpos];
	 stackfilter->parent = filtpos;
	 ++filter->count;
      }
      int emptycount = 0;
      for (uint32_t i = 0; i < prgstack.size(); ++i)
      {
	 prgstack[i - emptycount] = prgstack[i];
	 if (prgstack[i] == NULL)
	 {
	    ++emptycount;
	 }
	 if (emptycount > 0)
	 {
	    prgstack[i] = NULL;
	 }
      }
      if (emptycount == 0)
      {
	 prgstack.push_back(NULL);
	 emptycount = 1;
      }
      int stackpos = prgstack.size() - emptycount;
      prgstack[stackpos] = stackfilter;
      stackfilter->count = filter->count;
      uint blockstart=vm_read_data(input);
      if (firstbyte & 0x40)
      {
	 blockstart += 258;
      }
      stackfilter->start = (blockstart + window.get_position()) & WINDOW_SIZE_MASK;
      if (firstbyte & 0x20)
      {
	 stackfilter->length = vm_read_data(input);
      }
      else
      {
	 stackfilter->length = filtpos < oldfilterlengths.size() ? oldfilterlengths[filtpos] : 0;
      }
      stackfilter->nextwindow = writtenposi != window.get_position()
	 && ((writtenposi - window.get_position()) & WINDOW_SIZE_MASK) <= blockstart;
      oldfilterlengths[filtpos] = stackfilter->length;
      memset(stackfilter->prg.initr, 0, sizeof(stackfilter->prg.initr));
      stackfilter->prg.initr[3] = VM_GLOBALMEMADDR;
      stackfilter->prg.initr[4] = stackfilter->length;
      stackfilter->prg.initr[5] = stackfilter->count;
      if (firstbyte & 0x10)
      {
	 uint32_t initmask = input.getbits_8()>>1;
	 input.addbits(7);
	 for (uint32_t i = 0; i < 7; ++i)
	 {
	    if (initmask & (1<<i))
	    {
	       stackfilter->prg.initr[i] = vm_read_data(input);
	    }
	 }
      }
      if (newfilter)
      {
	 uint32_t vmcodesize = vm_read_data(input);
	 if (vmcodesize >= 0x10000 || vmcodesize == 0)
	    return false;
	 vector<uint8_t> newvmcode(vmcodesize);
	 for (uint i = 0; i < vmcodesize; ++i)
	 {
	    if (input.avail_size() <= 0)
	       return false;
	    newvmcode[i] = input.getbits_8();
	    input.addbits(8);
	 }
	 vm.prepare(&newvmcode[0], vmcodesize, &filter->prg);
      }
      stackfilter->prg.altcmd = &filter->prg.cmd[0];
      stackfilter->prg.cmdcount = filter->prg.cmdcount;
      size_t staticdatasize = filter->prg.staticdata.size();
      if (staticdatasize > 0 && staticdatasize < VM_GLOBALMEMSIZE)
      {
	 stackfilter->prg.staticdata = filter->prg.staticdata;
      }
      if (stackfilter->prg.globaldata.size() < VM_FIXEDGLOBALSIZE)
      {
	 stackfilter->prg.globaldata.resize(VM_FIXEDGLOBALSIZE);
      }
      uint8_t *globaldata = &stackfilter->prg.globaldata[0];
      for (uint32_t i = 0; i < 7; ++i)
	 vm.set_low_endian_value(reinterpret_cast<uint32_t*>(&globaldata[i * 4]), stackfilter->prg.initr[i]);
      vm.set_low_endian_value(reinterpret_cast<uint32_t*>(&globaldata[0x1c]), stackfilter->length);
      vm.set_low_endian_value(reinterpret_cast<uint32_t*>(&globaldata[0x20]), 0);
      vm.set_low_endian_value(reinterpret_cast<uint32_t*>(&globaldata[0x2c]), stackfilter->count);
      memset(&globaldata[0x30], 0, 16);
      if (firstbyte & 8)
      {
	 if (input.avail_size() <= 3)
	    return false;
	 uint32_t datasize = vm_read_data(input);
	 if (datasize > VM_GLOBALMEMSIZE - VM_FIXEDGLOBALSIZE)
	    return false;
	 size_t cursize = stackfilter->prg.globaldata.size();
	 if (cursize < datasize + VM_FIXEDGLOBALSIZE)
	 {
	    stackfilter->prg.globaldata.resize(datasize + VM_FIXEDGLOBALSIZE - cursize + stackfilter->prg.globaldata.size());
	 }
	 uint8_t* globaldata = &stackfilter->prg.globaldata[VM_FIXEDGLOBALSIZE];
	 for (uint32_t i = 0; i < datasize; ++i)
	 {
	    if (input.avail_size() <= 3)
	       return false;
	    globaldata[i] = input.getbits_8();
	    input.addbits(8);
	 }
      }
      return true;
   }
   
   void Unpack::unpack_29()
   {
      uint16_t bits;
      uint16_t number;
      int distnum;
      int length;
      int lengthnumber;
      size_t distance;
      int lowdist;
      int i;
      uint32_t tmpcrc = 0xffffffff;
      read_table();
      lowdistrepcount = 0;
      prevlowdist = 0;
      lastsize = 0;
      while (true)
      {
	 if (bitinput.avail_size() < 30)
	 {
	    bitinput.read();
	 }
	 if (window_avail_size() < 260)
	 {
	    write_to_file();
	 }
	 number = litdecode.get_number(bitinput);
	 if (number < 256)
	 {
	    window.add_byte((uint8_t)number);
	    continue;
	 }
	 if (number >= 271)
	 {
	    number -= 271;
	    length = ldecode[number] + 3;
	    bits = lbits[number];
	    if (bits > 0)
	    {
	       length += bitinput.getbits_16()>>(16-bits);
	       bitinput.addbits(bits);
	    }
	    distnum = distdecode.get_number(bitinput);
	    distance = ddecode[distnum] + 1;
	    bits = dbits[distnum];
	    if (bits > 0)
	    {
	       if (distnum > 9)
	       {
		  if (bits > 4)
		  {
		     distance += (bitinput.getbits_16()>>(20-bits))<<4;
		     bitinput.addbits(bits - 4);
		  }
		  if (lowdistrepcount > 0)
		  {
		     --lowdistrepcount;
		     distance += prevlowdist;
		  }
		  else
		  {
		     lowdist = lowdistdecode.get_number(bitinput);
		     if ( lowdist == 16 )
		     {
			lowdistrepcount = LOW_DIST_REP_COUNT - 1;
			distance += prevlowdist;
		     }
		     else
		     {
			distance += lowdist;
			prevlowdist = lowdist;
		     }
		  }
	       }
	       else
	       {
		  distance += bitinput.getbits_16() >> (16-bits);
		  bitinput.addbits(bits);
	       } // if (distnum > 9) ... else {
	    } // if (bits > 0) ... else {
	    if ( distance >= 0x2000 )
	    {
	       ++length;
	       if ( distance >= 0x40000 )
		  ++length;
	    }
	    insert_old_dist(distance);
	    insert_last_match(distance, length);
	    window.copy_string(distance, length);
	    continue;
	 }
	 if (number == 256)
	 {
	    if (read_end_of_block())
	       continue;
	    break;
	 }
	 if (number == 257)
	 {
	    if (read_vm())
	       continue;
	    break;
	 }
	 if (number == 258)
	 {
	    if (lastsize != 0)
	    {
	       window.copy_string(lastdist, lastsize);
	    }
	    continue;
	 }
	 if (number < 263)
	 {
	    distnum = number - 259;
	    distance = olddist[distnum];
	    for (i = distnum; i >0; --i)
	    {
	       olddist[i] = olddist[i - 1];
	    }
	    olddist[0] = distance;
	    lengthnumber = repdecode.get_number(bitinput);
	    length = ldecode[lengthnumber] + 2;
	    bits = lbits[lengthnumber];
	    if (bits > 0)
	    {
	       length += bitinput.getbits_16() >> (16 - bits);
	       bitinput.addbits(bits);
	    }
	    insert_last_match(distance, length);
	    window.copy_string(distance, length);
	    continue;
	 }
	 if (number < 272)
	 {
	    number -= 263;
	    distance = sddecode[number] + 1;
	    bits = sdbits[number];
	    if (bits > 0)
	    {
	       distance += bitinput.getbits_16() >> (16 - bits);
	       bitinput.addbits(bits);
	    }
	    insert_old_dist(distance);
	    insert_last_match(distance, 2);
	    window.copy_string(distance, 2);
	    continue;
	 }
      } // while (true) {
      write_to_file();
      dprint("writtensize=%lld\n", fw.get_writtensize());
      //dprint("readsize=%d\n", bitinput.gs());
      
      tmpcrc = fw.get_crc() ^ 0xffffffff;
      dprint("filecrc in filehead=%u\n", filecrc);
      dprint("filecrc=%u\n", tmpcrc);
      if ( tmpcrc != filecrc )
      {
	 fprintf(stderr, "file crc error");
      }
   }

   void Unpack::initfilters()
   {
      oldfilterlengths.clear();
      lastfilter = 0;
      for (auto i = filters.begin(); i != filters.end(); ++i)
      {
	 delete *i;
      }
      filters.clear();
      for (auto i = prgstack.begin(); i != prgstack.end(); ++i)
      {
	 delete *i;
      }
      prgstack.clear();
   }

   uint32_t Unpack::window_avail_size()
   {
      return window.get_maxsize() - (window.get_maxsize() - writtenposi + window.get_position()) % window.get_maxsize();
   }

   void Unpack::write_area(uint32_t start, uint32_t end)
   {
      if (start < end)
      {
	 fw.write(window.get_data() + start, end - start);
      }
      else if (start > end)
      {
	 fw.write(window.get_data() + start, window.get_maxsize() - start);
	 fw.write(window.get_data(), end);
      }
   }
   
   void Unpack::execute_code(VMPreparedProgram *prg)
   {
      if (prg->globaldata.size() > 0)
      {
	 prg->initr[6] = static_cast<uint32_t>(fw.get_writtensize());
	 vm.set_low_endian_value(reinterpret_cast<uint32_t*>(&prg->globaldata[0x24]), static_cast<uint32_t>(fw.get_writtensize()));
	 vm.set_low_endian_value(reinterpret_cast<uint32_t*>(&prg->globaldata[0x28]), static_cast<uint32_t>(fw.get_writtensize()));
	 vm.execute(prg);
      }
   }

   void Unpack::write_to_file()
   {
      uint32_t writtenborder = writtenposi;
      uint32_t writesize = (window.get_maxsize() - window_avail_size()) % window.get_maxsize();
      for (size_t i = 0; i < prgstack.size(); ++i)
      {
	 Filter *flt = prgstack[i];
	 if (flt == NULL)
	 {
	    continue;
	 }
	 if (flt->nextwindow)
	 {
	    flt->nextwindow = false;
	    continue;
	 }
	 uint32_t blockstart = flt->start;
	 uint32_t blocklength = flt->length;
	 if (((window.get_maxsize() + blockstart - writtenborder) % window.get_maxsize()) < writesize)
	 {
	    if (writtenborder != blockstart)
	    {
	       write_area(writtenborder, blockstart);
	       writtenborder = blockstart;
	       writesize = (window.get_maxsize() + window.get_position() - writtenborder) % window.get_maxsize();
	    }
	    if (blocklength <= writesize)
	    {
	       uint32_t blockend = (blockstart + blocklength) % window.get_maxsize();
	       if (blockstart < blockend || blockend == 0)
	       {
		  vm.set_memory(0, window.get_data() + blockstart, blocklength);
	       }
	       else
	       {
		  uint32_t firstpartlength = window.get_maxsize() - blockstart;
		  vm.set_memory(0, window.get_data() + blockstart, firstpartlength);
		  vm.set_memory(firstpartlength, window.get_data(), blockend);
	       }
	       VMPreparedProgram *parentprg = &filters[flt->parent]->prg;
	       VMPreparedProgram *prg = &flt->prg;
	       if (parentprg->globaldata.size() > VM_FIXEDGLOBALSIZE)
	       {
		  prg->globaldata.resize(parentprg->globaldata.size());
		  memcpy(&prg->globaldata[VM_FIXEDGLOBALSIZE], &parentprg->globaldata[VM_FIXEDGLOBALSIZE], parentprg->globaldata.size() - VM_FIXEDGLOBALSIZE);
	       }
	       execute_code(prg);
	       dprint("%u\t", *prg->filtereddata);
	       if (prg->globaldata.size() > VM_FIXEDGLOBALSIZE)
	       {
		  if (parentprg->globaldata.size() < prg->globaldata.size())
		  {
		     parentprg->globaldata.resize(prg->globaldata.size());
		  }
		  memcpy(&parentprg->globaldata[VM_FIXEDGLOBALSIZE], &prg->globaldata[VM_FIXEDGLOBALSIZE], prg->globaldata.size() - VM_FIXEDGLOBALSIZE);
	       }
	       else
		  parentprg->globaldata.clear();
	       uint8_t *filtereddata = prg->filtereddata;
	       uint32_t filtereddatasize = prg->filtereddatasize;
	       dprint("%u\t%u\n", *filtereddata, *prg->filtereddata);
	       
	       delete prgstack[i];
	       prgstack[i] = NULL;
	       while (i + 1 < prgstack.size())
	       {
		  Filter *nextfilter = prgstack[i + 1];
		  if (nextfilter == NULL || nextfilter->start != blockstart
		      || nextfilter->length != filtereddatasize
		      || nextfilter->nextwindow)
		  {
		     break;
		  }
		  vm.set_memory(0, filtereddata, filtereddatasize);
		  VMPreparedProgram *parentprg = &filters[nextfilter->parent]->prg;
		  VMPreparedProgram *nextprg = &nextfilter->prg;
		  if (parentprg->globaldata.size() > VM_FIXEDGLOBALSIZE)
		  {
		     nextprg->globaldata.resize(parentprg->globaldata.size());
		     memcpy(&nextprg->globaldata[VM_FIXEDGLOBALSIZE], &parentprg->globaldata[VM_FIXEDGLOBALSIZE], parentprg->globaldata.size() - VM_FIXEDGLOBALSIZE);
		  }
		  execute_code(nextprg);
		  if (nextprg->globaldata.size() > VM_FIXEDGLOBALSIZE)
		  {
		     if (parentprg->globaldata.size() < nextprg->globaldata.size())
		     {
			parentprg->globaldata.resize(nextprg->globaldata.size());
		     }
		     memcpy(&parentprg->globaldata[VM_FIXEDGLOBALSIZE], &nextprg->globaldata[VM_FIXEDGLOBALSIZE], nextprg->globaldata.size() - VM_FIXEDGLOBALSIZE);
		  }
		  else
		  {
		     parentprg->globaldata.clear();
		  }
		  filtereddata = nextprg->filtereddata;
		  filtereddatasize = nextprg->filtereddatasize;
		  ++i;
		  delete prgstack[i];
		  prgstack[i] = NULL;
	       }
	       fw.write(filtereddata, filtereddatasize);
	       writtenborder = blockend;
	       writesize = (window.get_maxsize() + window.get_position() - writtenborder) % window.get_maxsize();
	    }
	    else
	    {
	       for (size_t j = i; j < prgstack.size(); ++j)
	       {
		  Filter *flt = prgstack[j];
		  if (flt != NULL && flt->nextwindow)
		  {
		     flt->nextwindow = false;
		  }
		  writtenposi = writtenborder;
		  return;
	       }
	    }
	 }
      }
      
	 write_area(writtenborder, window.get_position());
	 writtenposi = window.get_position();
   }
   
} // end of namespace fur
